Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(middleware): Return a promise that resolves when middleware stack done #1339

Closed
wants to merge 11 commits into from

Conversation

timkinnane
Copy link

@timkinnane timkinnane commented May 22, 2017

Fixes #1338
Related #1279

Needs help / feedback...

  • The sinon fake timers don't seem to effect setTimeout, so it's actually taking the full half second to run the tests.
  • How should I reject the promise if the middleware is cancelled or throws?
  • I can change inline comments with new return values, but how should I update published docs?

@timkinnane
Copy link
Author

Just noticed Travis failed, but only on Node 0.1.0 due to lack of promise support.
How should I resolve that? I could use bluebird to polyfill, but I'm not sure about the etiquette of adding deps with PRs. Or is it time to ask if node <1 support is still required? Especially with new roadmap features coming up that might have similar support requirements.

@mose
Copy link
Member

mose commented May 22, 2017

Hubot 3 will certainly have new node version requirement, but there is still many 0.10 out there (cf #1336), I think it would be bad to introduce a breaking change for them at this point.

@timkinnane
Copy link
Author

timkinnane commented May 22, 2017

Aha, fair play. What then? Can I add a dependency - e.g. promise or bluebird?

@bkeepers
Copy link
Contributor

@timkinnane thanks for the pull request! I think we should wait until the next major release to drop support for v0.10.

In the mean time, I'm not opposed to adding a dependency so we can start using promises now. es6-promise would be my preference so we can use the native Promise on every other version of node.

Promise = global.Promise || require('es6-promise').Promise

Builds in node 0.10.x were failing due to lack of promise support

1339
@technicalpickles
Copy link
Member

I can change inline comments with new return values, but how should I update published docs?

You can commit changes to docs, and those will automatically (or should) get updated on hubot.github.com

@@ -1,4 +1,5 @@
async = require 'async'
Promise = global.Promise || require('es6-promise').Promise
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think https://github.com/stefanpenner/es6-promise#auto-polyfill means you could require the auto-polyfill, and then just use Promise. On the other hand, using this style is a lot more clear on where Promise is coming from.

@technicalpickles
Copy link
Member

How should I reject the promise if the middleware is cancelled or throws?

I think you'd need to get promises involved a little earlier in the execute. The way executeSingleMiddleware currently works, it either succeeds, or it emits an error and calls done.

Maybe if you wrap more of the method in the promise call, you'd be able to resolve/reject during those checks?

@timkinnane
Copy link
Author

timkinnane commented May 23, 2017

Sweet, I've made those changes and added docs.

I added one test that fails, because I need some advice to fix it.
How can I make the promise resolve early if a middleware calls done, before final done?

The rejection part worked though.

@@ -238,7 +296,7 @@ describe 'Middleware', ->
{}
middlewareFinished
middlewareFailed
)
).catch (reason) -> # supress warning re unhandled promise rejection
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this start showing unexpected warnings when people upgrade?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without that line 299, it was logging a warning in the tests, where middleware threw. Saying "unhandled promise rejection" - not a big deal, but simply catching the promise and doing nothing with it removes the warning. It would only effect people who's middleware is also throwing, in which case they'd see errors anyway.

@bkeepers
Copy link
Contributor

bkeepers commented May 23, 2017

I added one test that fails, because I need some advice to fix it.
How can I make the promise resolve early if a middleware calls done, before final done?

I'm having trouble wrapping my head around this at the moment. You basically want calling done to be the same as resolving the promise?

@timkinnane
Copy link
Author

@bkeepers yeah, so any middleware in the stack can prematurely end it, but that's not really a fail, so the promise should still resolve with the context at that point. I just can't see how to hook in the resolve method at that point. The way I've done it only works through the final done function.

The test I added at line 240 demos how it should work. I'm sure I'll be able to fix this myself, just wanted throw it up in case someone has a simple solution.

@timkinnane
Copy link
Author

Fixed the test I added, for resolving promise early if middleware calls done()

@robot.emit('error', err, context.response)
# Forcibly fail the middleware and stop executing deeper
doneFunc()
reject err, context
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise.reject() only accepts one argument, you can try it out using

Promise.reject(new Error('foo'), {}).catch(function (error, context) {
  console.log(context)
})
// logs `undefined`

What you could do is set context as property of err before rejecting with it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one. Thanks for that info, I'll do it that way instead.

@@ -702,6 +702,10 @@ Every middleware receives the same API signature of `context`, `next`, and
`done`. Different kinds of middleware may receive different information in the
`context` object. For more details, see the API for each type of middleware.

Middleware execution returns a promise that resolves with the final `context`
when the middleware stack completes. If the middleware stack throws an error,
the promise will be rejected with the error and `context` at that point.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see my other comment, a promise can only reject with one argument

@gr2m
Copy link
Contributor

gr2m commented Jun 12, 2017

hey @timkinnane I gave your PR finally a more thorough look today.

One thing that really confuses me is this part:

middleware in the stack can prematurely end it,

I try to wrap my head around it but don’t understand why that is. Could someone explain it to me? Is it so that a there can be an array of registered middleware which all do their own checks and if one of them finds that the request is for them, then we don’t want the next middleware to even do its check?

I know your PR did not introduce this but I’m still rather new to the code base and it would help me to understand the thinking behind this pattern /cc @technicalpickles

@timkinnane
Copy link
Author

timkinnane commented Jun 12, 2017

@gr2m, so normally it would run all middleware methods in the order they're registered, then a final function when complete, but each middleware in the queue can end the processing of the stack. The result of finishing early is that the "final" function never runs, but it isn't necessarily an error, just a different kind of conclusion. e.g. A listener middleware might prevent further middleware from processing if the user isn't authorised to access the listener, then reply with some "you are not valid" message instead of proceeding to the response callback.

In such cases the promise should still resolve even though the stack was "cancelled" because middleware.execute still completed without throwing and you might want to write asynchronous logic that will interrogate the context of the middleware, wether it was authorised or not.

I'm currently away and will be for another week, but I'll put more work into this when I get back, to help it get through with a more consistent approach if required.

@gr2m
Copy link
Contributor

gr2m commented Jun 12, 2017

thanks for the clarifications! Note that we will release a new major version of hubot soon which is now based on JavaScript and has a few minor breaking changes. But if for some reason you can’t upgrade to it we can release another 2.x release with the new feature, given we agree that we want to add it

@timkinnane
Copy link
Author

timkinnane commented Jun 12, 2017

Cheers, I've written a lot of code for 2.x and the promise feature is most useful for me when running mocha tests of those scripts. So I'd rather have it available without needing to update those scripts, but I'll look to evolve my packages to work better with v3 over time.

timkinnane added 4 commits June 29, 2017 16:28
# Conflicts:
#	src/middleware.coffee
#	src/response.coffee
#	src/robot.coffee
#	test/middleware_test.coffee

Migrated promise features from removed coffee into js
@timkinnane
Copy link
Author

Made a couple new branches to resubmit this separately for v2 and v3

@timkinnane timkinnane closed this Jul 14, 2017
@timkinnane timkinnane deleted the middleware-promises branch July 14, 2017 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants